package com.pspaceplus.driver.bacnetdemo.init;

import com.pspaceplus.driver.bacnetdemo.util.BACnetUtil;
import com.pspaceplus.driver.bacnetdemo.util.IpUtil;
import com.serotonin.bacnet4j.LocalDevice;
import com.serotonin.bacnet4j.RemoteDevice;
import com.serotonin.bacnet4j.event.DeviceEventAdapter;
import com.serotonin.bacnet4j.exception.BACnetException;
import com.serotonin.bacnet4j.npdu.ip.IpNetwork;
import com.serotonin.bacnet4j.service.acknowledgement.AcknowledgementService;
import com.serotonin.bacnet4j.service.confirmed.SubscribeCOVPropertyRequest;
import com.serotonin.bacnet4j.service.confirmed.SubscribeCOVRequest;
import com.serotonin.bacnet4j.type.constructed.PropertyReference;
import com.serotonin.bacnet4j.type.constructed.PropertyValue;
import com.serotonin.bacnet4j.type.constructed.SequenceOf;
import com.serotonin.bacnet4j.type.constructed.TimeStamp;
import com.serotonin.bacnet4j.type.enumerated.*;
import com.serotonin.bacnet4j.type.notificationParameters.NotificationParameters;
import com.serotonin.bacnet4j.type.primitive.Boolean;
import com.serotonin.bacnet4j.type.primitive.CharacterString;
import com.serotonin.bacnet4j.type.primitive.ObjectIdentifier;
import com.serotonin.bacnet4j.type.primitive.UnsignedInteger;
import com.serotonin.bacnet4j.util.*;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
 * @ClassName: run
 * @Description: TODO-wcy
 * @Author: wcy
 * @Date: 2022/4/25 16:01
 * @Version: 1.0
 */
@Component
@Order(1)
public class TestBACnetForSubscribe implements ApplicationRunner {

    LocalDevice localDevice;

    RemoteDevice remoteDevice;

    DeviceEventAdapter covPropertyEventAdapter;

    DeviceEventAdapter eventAdapter;

    @Override
    public void run(ApplicationArguments args) throws Exception {
//        String ip = "172.19.83.237";
//        String broadcast = "172.19.83.255";
//        int remoteDeviceNumber = 260002;

        String ip = IpUtil.getIpAddress(); // 本机ip
        String subnet = IpUtil.getSubnet(); // 本机子网掩码
        IpNetwork ipNetwork = BACnetUtil.initIpNetwork(ip, subnet);

        // 初始化一个虚拟的本地设备，用于发送请求
        int localDeviceNumber = 6451; // 任意写，和已有设备id重复也没关系
        localDevice = BACnetUtil.initLocalDevice(ipNetwork, localDeviceNumber);

        // 订阅值的变化 COV
        int remoteDeviceIdForSubscribeCOV = 1425616; // 要写实际的设备id，此处是以 Yabe 软件自带的 Bacnet.Room.Simulator 模拟器为例
        ObjectIdentifier objectIdentifierForSubscribeCOV = new ObjectIdentifier(ObjectType.analogInput, 0);
        PropertyIdentifier propertyForSubscribe = PropertyIdentifier.presentValue;

        remoteDevice = RemoteDeviceFinder.findDevice(localDevice, remoteDeviceIdForSubscribeCOV).get();
        DiscoveryUtils.getExtendedDeviceInformation(localDevice, remoteDevice);

        AcknowledgementService subcribeAcknowledgementService = subscribePropertyCOV(objectIdentifierForSubscribeCOV, propertyForSubscribe);

        // 暂停几秒钟，观察接收到的值变化的信息
        Thread.sleep(6000);

        // 取消订阅值的变化 COV
        AcknowledgementService unsubscribeAcknowledgementService = unSubscribePropertyCOV(objectIdentifierForSubscribeCOV, propertyForSubscribe);

        // 事件
        // ！！！！测试事件失败！在包中订阅相关的Request只看到了COV的，没有看到有其他类型的事件订阅的Request，而且也不知道事件是怎么在设备上触发的，so，完全没有头绪，就写了个 SubscribeCOVRequest 试了试，发现没啥效果。
        subscribeEvent(objectIdentifierForSubscribeCOV, propertyForSubscribe);

        Thread.sleep(3000);

        unSubscribeEvent(objectIdentifierForSubscribeCOV, propertyForSubscribe);

        // 方法

        // 终止本地虚拟设备
        localDevice.terminate();
        System.out.println("本次测试终止");
    }

    private void subscribeEvent(ObjectIdentifier objectIdentifierForSubscribe, PropertyIdentifier propertyForSubscribe) throws BACnetException {
        String subscribeTargetStr = "对象: " + objectIdentifierForSubscribe.getObjectType() + " " + objectIdentifierForSubscribe.getInstanceNumber();
        System.out.println("************************** 订阅事件 " + subscribeTargetStr + " ****************************");

        // 每次订阅时，有个lifetime的参数，指定本次订阅的持续时间，到时间后此订阅将过期，如想继续订阅就需继续发送订阅请求。设置为 0 或者 null 应该是永不过期（测试了一分钟，一分钟内一直在获取订阅数据）
        UnsignedInteger lifeTime = new UnsignedInteger(10);
        SubscribeCOVRequest subscribeCOVRequest = new SubscribeCOVRequest(new UnsignedInteger(5),
                objectIdentifierForSubscribe,
                Boolean.TRUE,
                lifeTime);

        // 构建自定义监听器，处理event（包中有两个相关类 com.serotonin.bacnet4j.obj.NotificationForwarderObject, com.serotonin.bacnet4j.obj.EventLogObject，不知道什么用处）
        eventAdapter = new DeviceEventAdapter() {
            @Override
            public void eventNotificationReceived(final UnsignedInteger processIdentifier,
                                                  final ObjectIdentifier initiatingDeviceIdentifier, final ObjectIdentifier eventObjectIdentifier,
                                                  final TimeStamp timeStamp, final UnsignedInteger notificationClass, final UnsignedInteger priority,
                                                  final EventType eventType, final CharacterString messageText, final NotifyType notifyType,
                                                  final Boolean ackRequired, final EventState fromState, final EventState toState,
                                                  final NotificationParameters eventValues) {
                DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
                System.out.print("event received " + dateTimeFormatter.format(LocalDateTime.now())
                        + ", processIdentifier: " + processIdentifier
                        + ", initiatingDeviceIdentifier: " + initiatingDeviceIdentifier.toString()
                        + ", eventObjectIdentifier: " + eventObjectIdentifier.toString()
                        + ", timeStamp: " + timeStamp.toString()
                        + ", notificationClass: " + notificationClass
                        + ", priority: " + priority
                        + ", eventType: " + eventType.toString()
                        + ", messageText: " + messageText
                        + ", notifyType: " + notifyType.toString()
                        + ", ackRequired: " + ackRequired
                        + ", fromState: " + fromState.toString()
                        + ", toState: " + toState.toString()
                        + ", eventValues: " + eventValues.toString());
                System.out.println();
            }
        };
        localDevice.getEventHandler().addListener(eventAdapter);

        AcknowledgementService acknowledgementService = localDevice.send(remoteDevice, subscribeCOVRequest).get();

        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        System.out.println("订阅事件成功,本次订阅开始时间:" + dateTimeFormatter.format(LocalDateTime.now()) + " -- " + subscribeTargetStr);
    }

    private void unSubscribeEvent(ObjectIdentifier objectIdentifierForSubscribe, PropertyIdentifier propertyForSubscribe) throws BACnetException {
        String subscribeTargetStr = "对象: " + objectIdentifierForSubscribe.getObjectType() + " " + objectIdentifierForSubscribe.getInstanceNumber();
        System.out.println("************************** 取消订阅事件 " + subscribeTargetStr + " ****************************");

        SubscribeCOVRequest unSubscribeCOVRequest = new SubscribeCOVRequest(new UnsignedInteger(5),
                objectIdentifierForSubscribe,
                null,
                null);
        AcknowledgementService acknowledgementService = localDevice.send(remoteDevice, unSubscribeCOVRequest).get();

        localDevice.getEventHandler().removeListener(eventAdapter);

        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        System.out.println("取消订阅成功,取消时间:" + dateTimeFormatter.format(LocalDateTime.now()));
    }

    private AcknowledgementService subscribePropertyCOV(ObjectIdentifier objectIdentifierForSubscribeCOV, PropertyIdentifier propertyForSubscribe) throws BACnetException {
        String subscribeTargetStr = "对象: " + objectIdentifierForSubscribeCOV.getObjectType() + " " + objectIdentifierForSubscribeCOV.getInstanceNumber() + ", 属性名: " + propertyForSubscribe.toString();
        System.out.println("************************** 订阅对象中的某个属性 " + subscribeTargetStr + " ****************************");

        // 每次订阅时，有个lifetime的参数，指定本次订阅的持续时间，到时间后此订阅将过期，如想继续订阅就需继续发送订阅请求。设置为 0 或者 null 应该是永不过期（测试了一分钟，一分钟内一直在获取订阅数据）
        UnsignedInteger lifeTime = new UnsignedInteger(10);
        // TODO-wcy-normal：应该也可以通过构建 SubscribeCOVRequest，订阅对象的变化，不过未测试，不确定是否可行
        // TODO-wcy-high：代码示例中好像有批量订阅的方式，待研究。2022/04/29 14:44:19
        // 示例参考：源码中test目录下的：com.serotonin.bacnet4j.obj.ChangeOfValueTest
        // 第一个参数：订阅标识，不可为0
        SubscribeCOVPropertyRequest subscribeCOVPropertyRequest = new SubscribeCOVPropertyRequest(new UnsignedInteger(4),
                objectIdentifierForSubscribeCOV,
                Boolean.TRUE,
                lifeTime,
                new PropertyReference(propertyForSubscribe),
                null);

        // 构建自定义监听器，处理COV收到的值（包中只有一个实现类 com.serotonin.bacnet4j.obj.CovNotifListener，没有提供自定义逻辑入口，无法使用，只能自己新建类了）
        covPropertyEventAdapter = new DeviceEventAdapter() {
            @Override
            public void covNotificationReceived(final UnsignedInteger subscriberProcessIdentifier,
                                                final ObjectIdentifier initiatingDeviceIdentifier, final ObjectIdentifier monitoredObjectIdentifier,
                                                final UnsignedInteger timeRemaining, final SequenceOf<PropertyValue> listOfValues) {
                DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
                System.out.print("COV received " + dateTimeFormatter.format(LocalDateTime.now())
                        + ", subscriberProcessIdentifier: " + subscriberProcessIdentifier
                        + ", initiatingDeviceIdentifier: " + initiatingDeviceIdentifier.toString()
                        + ", monitoredObjectIdentifier: " + monitoredObjectIdentifier.toString()
                        + ", timeRemaining: " + timeRemaining
                        + ", listOfValues: ");
                listOfValues.getValues().forEach(propertyValue -> {
                    System.out.print("{ propertyName: " + propertyValue.getPropertyIdentifier().toString() + ", value: " + propertyValue.getValue() + " }, ");
                });
                System.out.println();
            }
        };
        localDevice.getEventHandler().addListener(covPropertyEventAdapter);

        AcknowledgementService acknowledgementService = localDevice.send(remoteDevice, subscribeCOVPropertyRequest).get();

        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        System.out.println("订阅COV成功,本次订阅开始时间:" + dateTimeFormatter.format(LocalDateTime.now()) + ", 本次订阅周期:" + lifeTime + "秒" + " -- " + subscribeTargetStr);

        return acknowledgementService;
    }

    private AcknowledgementService unSubscribePropertyCOV(ObjectIdentifier objectIdentifierForSubscribeCOV, PropertyIdentifier propertyForSubscribe) throws BACnetException {
        String subscribeTargetStr = "对象: " + objectIdentifierForSubscribeCOV.getObjectType() + " " + objectIdentifierForSubscribeCOV.getInstanceNumber() + ", 属性名: " + propertyForSubscribe.toString();
        System.out.println("************************** 取消订阅对象中的某个属性 " + subscribeTargetStr + " ****************************");

        SubscribeCOVPropertyRequest unSubscribeCOVPropertyRequest = new SubscribeCOVPropertyRequest(new UnsignedInteger(4),
                objectIdentifierForSubscribeCOV,
                null,
                null,
                new PropertyReference(PropertyIdentifier.presentValue),
                null);
        AcknowledgementService acknowledgementService = localDevice.send(remoteDevice, unSubscribeCOVPropertyRequest).get();

        localDevice.getEventHandler().removeListener(covPropertyEventAdapter);

        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        System.out.println("取消订阅成功,取消时间:" + dateTimeFormatter.format(LocalDateTime.now()));

        return acknowledgementService;
    }
}
